﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BFlatCompile.Serializer;

namespace BFlatCompile
{
	public class Compiler
	{
		public string[] InputFiles { get; private set; }
		public string[] CopyFiles { get; private set; }

		public bool CompileWindows { get; private set; }
		public bool CompileJavascript { get; private set; }
		public bool IsReadable { get; private set; }
		public string Name { get; private set; }
		public string CanonicalName { get; private set; }

		public string ProjectRoot { get; private set; }
		public string OutputRootPrefix { get; private set; }

		internal UniqueStringByIdTable StackTraceTokenTable { get; private set; }

		private string CanonicalizeName(string name)
		{
			string output = "";
			char c;
			for (int i = 0; i < name.Length; ++i)
			{
				c = name[i];
				if ((c >= 'a' && c <= 'z') ||
					(c >= 'A' && c <= 'Z') ||
					(c >= '0' && c <= '9'))
				{
					output += c;
				}
				else
				{
					output += '_';
				}
			}

			output = output.Trim('_');
			output = output.Length == 0 ? "Untitled_Project" : output;
			return output;
		}

		public Compiler(string[] args)
		{
			this.StackTraceTokenTable = new UniqueStringByIdTable();
			this.ApplyFlags(args);
		}

		private void ApplyFlags(string[] args)
		{
			if (args.Length == 0)
			{
				throw new InvalidUsageException(string.Join("\n", new string[] {
					"Usage:",
					" BFlatCompile.exe {path to .bfp file}",
					"",
					"Flags:",
					" -windows",
					"    Create Windows .exe output.",
					"",
					" -js",
					"    Create Javascript output.",
					"",
					" -readable",
					"    Create human-readable byte code file. (Cannot execute)"
				}));
			}

			string bfp = args[0];

			if (!bfp.ToLowerInvariant().EndsWith(".bfp"))
			{
				throw new InvalidUsageException("Only B Flat Projects with a .bfp extension are valid.");
			}

			if (!System.IO.Path.IsPathRooted(bfp))
			{
				string currentDirectory = System.IO.Directory.GetCurrentDirectory();
				bfp = System.IO.Path.Combine(currentDirectory, bfp);
			}


			if (!System.IO.File.Exists(bfp))
			{
				throw new InvalidUsageException("The file does not exist: " + bfp);
			}

			string directory = System.IO.Path.GetDirectoryName(bfp);
			this.ProjectRoot = directory;
			string projectFileContents = System.IO.File.ReadAllText(bfp);
			Project.JsonParser projectParser = new Project.JsonParser(projectFileContents);
			Project.JsonObject root = projectParser.Parse();
			string name = root.TryGetString("name") ?? "Untitled_Project";
			this.Name = name;
			this.CanonicalName = this.CanonicalizeName(name);
			string version = root.TryGetString("version");
			// TODO: canonicalize version into file-friendly format.
			this.OutputRootPrefix = this.CanonicalName + "_" + version + "_";

			string[] compileTargets = Project.ProjectUtil.FlattenCompileTargets(directory, root);
			string[] copyTargets = Project.ProjectUtil.FlattenCopyTargets(directory, root);

			this.InputFiles = compileTargets;
			this.CopyFiles = copyTargets;

			int platformTargets = 0;

			for (int i = 1; i < args.Length; ++i)
			{
				switch (args[i])
				{
					case "-windows":
						this.CompileWindows = true;
						++platformTargets;
						break;

					case "-js":
						this.CompileJavascript = true;
						++platformTargets;
						break;

					case "-readable":
						this.IsReadable = true;
						break;

					default:
						throw new ParserException("Invalid command line flag: " + args[i]);
				}
			}

			if (platformTargets == 0)
			{
				throw new InvalidUsageException("No platform targets were selected. See flags.");
			}
		}

		private class FileSource
		{
			public string FileName { get; private set; }
			public string RelativeFileName { get; private set; }
			public string Contents { get; private set; }
			public bool IsBuiltIn { get { return false; } set { } }//{ get; private set; }

			private FileSource(string filename, string contents, bool builtIn, string relativeFilename)
			{
				this.FileName = filename;
				this.RelativeFileName = relativeFilename;
				this.Contents = SanitizeLineEndings(contents);
				this.IsBuiltIn = builtIn;
			}

			private static string SanitizeLineEndings(string rawCode)
			{
				return rawCode
					.Replace("\r\n", "\n")
					.Replace('\r', '\n');
			}

			public static FileSource FromFile(string filename, string shortFilename)
			{
				string contents = System.IO.File.ReadAllText(filename);
				return new FileSource(filename, contents, false, shortFilename);
			}

			public static FileSource BuiltIns(string internalPath)
			{
				System.Reflection.Assembly thisBinary = typeof(Compiler).Assembly;
				string[] foo = typeof(Compiler).Assembly.GetManifestResourceNames();
				string path = "BFlatCompile.BuiltIns." + internalPath.Replace('/', '.');
				System.IO.Stream stream = thisBinary.GetManifestResourceStream(path);
				string content = GetStringFromStream(stream);
				string filename = "$/" + internalPath;
				return new FileSource(filename, content, true, filename);
			}

			private static string GetStringFromStream(System.IO.Stream stream)
			{
				StringBuilder output = new StringBuilder();
				int value;
				do
				{
					value = stream.ReadByte();
					if (value != -1)
					{
						output.Append((char)value);
					}
				} while (value != -1);
				string content = output.ToString();
				
				if (content[0] == 239 && content[1] == 187 && content[2] == 191)
				{
					content = content.Substring(3);
				}

				return content;
			}
		}

		internal string GetFileFromPath(string path)
		{
			FileSource file;
			if (this.fileSources.TryGetValue(path, out file))
			{
				return file.Contents;
			}

			return null;
		}

		private Dictionary<string, FileSource> fileSources = new Dictionary<string, FileSource>();

		public void Compile()
		{
			List<FileSource> sources = new List<FileSource>();

			foreach (string inputFile in this.InputFiles)
			{
				FileSource file = FileSource.FromFile(inputFile, inputFile.Substring(this.ProjectRoot.Length));
				sources.Add(file);
				this.fileSources[inputFile] = file;
			}

			foreach (string path in new string[] {
				"System/Console.bf",
				"System/Exception.bf",
				"System/Math.bf",
				"System/PrimitiveMethods.bf",
				"System/Random.bf",
				"System/Time.bf",
				"OS/File.bf",
				"Game/Image.bf",
				"Game/Screen.bf",
				"Game/Event.bf",
				"Game/Window.bf",
				"Input/Key.bf" })
			{
				sources.Add(FileSource.BuiltIns(path));
			}

			List<Nodes.Program> programChunks = new List<Nodes.Program>();
			
			foreach (FileSource source in sources)
			{
				Tokens tokens = new Tokens(source.FileName, source.Contents, source.IsBuiltIn, source.RelativeFileName);
				Parser parser = new Parser();
				parser.AddTokenSet(tokens);
				Nodes.Program program = parser.Parse();
				programChunks.Add(program);
			}

			while (programChunks.Count > 1)
			{
				programChunks[0].Merge(programChunks[1]);
				programChunks.RemoveAt(1);
			}

			Nodes.Program compositedProgram = programChunks[0];

			Dictionary<string, string> components = MetadataSimplifier.Simplify(compositedProgram, this);
			List<string> parts = new List<string>();
			foreach (string key in components.Keys)
			{
				string value = components[key];
				if (!this.IsReadable)
				{
					value = key + value;
					value = Util.ToHex(this, value.Length, 8) + value;
				}
				else
				{
					value = "SECTION: " + key + "\n" + value + "\n\n";
				}

				parts.Add(value);
			}

			string compiledCode = string.Join("", parts);

			if (this.CompileWindows) this.CreateOutputDirectory(compiledCode, CompileType.Windows, "windows");
			if (this.CompileJavascript) this.CreateOutputDirectory(compiledCode, CompileType.JavaScript, "js");
		}

		private enum CompileType
		{
			Windows,
			JavaScript,
		}

		private void GetAllFiles(string directory, List<string> output)
		{
			foreach (string file in System.IO.Directory.GetDirectories(directory))
			{
				GetAllFiles(System.IO.Path.Combine(directory, file), output);
			}

			foreach (string file in System.IO.Directory.GetFiles(directory))
			{
				output.Add(System.IO.Path.Combine(directory, file));
			}
		}

		private void CreateOutputDirectory(string compiledCode, CompileType compileType, string outputPathSuffix)
		{
			string targetDirectory = System.IO.Path.Combine(this.ProjectRoot, this.OutputRootPrefix + outputPathSuffix);
			if (System.IO.Directory.Exists(targetDirectory))
			{
				List<string> allOldFiles = new List<string>();
				GetAllFiles(targetDirectory, allOldFiles);
				foreach (string oldFile in allOldFiles)
				{
					try
					{
						System.IO.File.Delete(oldFile);
					}
					catch (Exception)
					{
						throw new ParserException("This file is still being held on to by the operating system.");
					}
				}
			}
			else
			{
				System.IO.Directory.CreateDirectory(targetDirectory);
			}

			string bfxFilename = this.CanonicalName + ".bfx";
			string outputBfx = System.IO.Path.Combine(targetDirectory, bfxFilename);
			System.IO.File.WriteAllText(outputBfx, compiledCode);
			foreach (string file in this.CopyFiles)
			{
				string relativePath = file.Substring(this.ProjectRoot.Length + 1);
				string absolutePath = System.IO.Path.Combine(targetDirectory, relativePath);

				EnsureDirectoryExists(System.IO.Path.GetDirectoryName(absolutePath));
				// TODO: verify this creates the directories, if non-existent.
				System.IO.File.Copy(file, absolutePath);
			}

			if (compileType == CompileType.Windows)
			{
				this.OutputWindowsBinaries(this.CanonicalName, bfxFilename, targetDirectory, true);
			}
		}

		// TODO: Automatically detect whether gameCore is required. Allow user to compile command line apps.
		private void OutputWindowsBinaries(string projectName, string bfxFile, string targetDirectory, bool includeGameCore)
		{
			byte[] gameCore = this.GetEmbeddedResource("OutputFiles/Windows/GameCore.dll");
			byte[] openTk = this.GetEmbeddedResource("OutputFiles/Windows/OpenTK.dll");
			byte[] interpreter = this.GetEmbeddedResource("OutputFiles/Windows/Interpreter.exe");

			int interpreterLength = interpreter.Length - 102;
			bool found = false;
			for (int i = 0; i < interpreterLength; ++i)
			{
				if (interpreter[i] == '*')
				{
					found = true;
					for (int j = 0; j < 100; ++j)
					{
						if (interpreter[i + j*2] != '*' ||
							interpreter[i + j*2 + 1] != 0)
						{
							found = false;
							break;
						}
					}

					if (found)
					{
						if (bfxFile.Length > 64)
						{
							throw new Exception("BFX file is way too long.");
						}

						for (int j = 0; j < bfxFile.Length; ++j)
						{
							interpreter[i + j * 2] = (byte)bfxFile[j];
						}
					}
				}
			}

			System.IO.File.WriteAllBytes(System.IO.Path.Combine(targetDirectory, "GameCore.dll"), gameCore);
			System.IO.File.WriteAllBytes(System.IO.Path.Combine(targetDirectory, "OpenTK.dll"), openTk);
			System.IO.File.WriteAllBytes(System.IO.Path.Combine(targetDirectory, projectName + ".exe"), interpreter);
		}

		private byte[] GetEmbeddedResource(string path)
		{
			string[] foo = typeof(Compiler).Assembly.GetManifestResourceNames();
			System.IO.Stream stream = typeof(Compiler).Assembly.GetManifestResourceStream("BFlatCompile." + path.Replace('/', '.'));
			List<byte> output = new List<byte>();
			byte[] buffer = new byte[4000];
			int offset = 0;
			int bytesRead = 0;
			do
			{
				bytesRead = stream.Read(buffer, 0, buffer.Length);
				if (bytesRead > 0)
				{
					output.AddRange(buffer);
					offset += bytesRead;
				}
			} while (bytesRead> 0);

			byte[] trueOutput = new byte[offset];
			output.CopyTo(0, trueOutput, 0, offset);

			return trueOutput;
		}

		private static void EnsureDirectoryExists(string path)
		{
			char osSep = System.IO.Path.DirectorySeparatorChar;
			if (path.IndexOf(osSep) != path.LastIndexOf(osSep))
			{
				EnsureDirectoryExists(System.IO.Path.GetDirectoryName(path));
			}

			if (!System.IO.Directory.Exists(path))
			{
				System.IO.Directory.CreateDirectory(path);
			}
		}
	}
}
